using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;

namespace UnityEditor.Timeline
{
    static class ClipModifier
    {
        public static bool Delete(TimelineAsset timeline, TimelineClip clip)
        {
            return timeline.DeleteClip(clip);
        }

        public static bool Tile(TimelineClip[] clips)
        {
            if (clips.Length < 2)
                return false;

            var clipsByTracks = clips.GroupBy(x => x.parentTrack)
                .Select(track => new {track.Key, Items = track.OrderBy(c => c.start)});

            foreach (var track in clipsByTracks)
            {
                TimelineUndo.PushUndo(track.Key, "Tile");
            }

            foreach (var track in clipsByTracks)
            {
                double newStart = track.Items.First().start;
                foreach (var c in track.Items)
                {
                    c.start = newStart;
                    newStart += c.duration;
                }
            }

            return true;
        }

        public static bool TrimStart(TimelineClip[] clips, double trimTime)
        {
            var result = false;

            foreach (var clip in clips)
                result |= TrimStart(clip, trimTime);

            return result;
        }

        public static bool TrimStart(TimelineClip clip, double trimTime)
        {
            if (clip.asset == null)
                return false;

            if (clip.start > trimTime)
                return false;

            if (clip.end < trimTime)
                return false;

            TimelineUndo.PushUndo(clip.parentTrack, "Trim Clip Start");

            // Note: We are NOT using edit modes in this case because we want the same result
            // regardless of the selected EditMode: split at cursor and delete left part
            SetStart(clip, trimTime);

            return true;
        }

        public static bool TrimEnd(TimelineClip[] clips, double trimTime)
        {
            var result = false;

            foreach (var clip in clips)
                result |= TrimEnd(clip, trimTime);

            return result;
        }

        public static bool TrimEnd(TimelineClip clip, double trimTime)
        {
            if (clip.asset == null)
                return false;

            if (clip.start > trimTime)
                return false;

            if (clip.end < trimTime)
                return false;

            TimelineUndo.PushUndo(clip.parentTrack, "Trim Clip End");
            TrimClipWithEditMode(clip, TrimEdge.End, trimTime);

            return true;
        }

        public static bool MatchDuration(TimelineClip[] clips)
        {
            double referenceDuration = clips[0].duration;
            foreach (var clip in clips)
            {
                TimelineUndo.PushUndo(clip.parentTrack, "Match Clip Duration");

                var newEnd = clip.start + referenceDuration;
                TrimClipWithEditMode(clip, TrimEdge.End, newEnd);
            }

            return true;
        }

        public static bool Split(TimelineClip[] clips, double splitTime, PlayableDirector director)
        {
            var result = false;

            foreach (var clip in clips)
            {
                if (clip.start >= splitTime)
                    continue;

                if (clip.end <= splitTime)
                    continue;

                TimelineUndo.PushUndo(clip.parentTrack, "Split Clip");

                TimelineClip newClip = TimelineHelpers.Clone(clip, director, director, clip.start);

                SetStart(clip, splitTime);
                SetEnd(newClip, splitTime, false);

                // Sort produced by cloning clips on top of each other is unpredictable (it varies between mono runtimes)
                clip.parentTrack.SortClips();

                result = true;
            }

            return result;
        }

        public static void SetStart(TimelineClip clip, double time)
        {
            var supportsClipIn = clip.SupportsClipIn();
            var supportsPadding = TimelineUtility.IsRecordableAnimationClip(clip);

            // treat empty recordable clips as not supporting clip in (there are no keys to modify)
            if (supportsPadding && (clip.animationClip == null || clip.animationClip.empty))
            {
                supportsClipIn = false;
            }

            if (supportsClipIn && !supportsPadding)
            {
                var minStart = clip.FromLocalTimeUnbound(0.0);
                if (time < minStart)
                    time = minStart;
            }

            var maxStart = clip.end - TimelineClip.kMinDuration;
            if (time > maxStart)
                time = maxStart;

            var timeOffset = time - clip.start;
            var duration = clip.duration - timeOffset;

            if (supportsClipIn)
            {
                if (supportsPadding)
                {
                    double clipInGlobal = clip.clipIn / clip.timeScale;
                    double keyShift = -timeOffset;
                    if (timeOffset < 0) // left drag, eliminate clipIn before shifting
                    {
                        double clipInDelta = Math.Max(-clipInGlobal, timeOffset);
                        keyShift = -Math.Min(0, timeOffset - clipInDelta);
                        clip.clipIn += clipInDelta * clip.timeScale;
                    }
                    else if (timeOffset > 0) // right drag, elimate padding in animation clip before adding clip in
                    {
                        var clipInfo = AnimationClipCurveCache.Instance.GetCurveInfo(clip.animationClip);
                        double keyDelta = clip.FromLocalTimeUnbound(clipInfo.keyTimes.Min()) - clip.start;
                        keyShift = -Math.Max(0, Math.Min(timeOffset, keyDelta));
                        clip.clipIn += Math.Max(timeOffset + keyShift, 0) * clip.timeScale;
                    }
                    if (keyShift != 0)
                    {
                        AnimationTrackRecorder.ShiftAnimationClip(clip.animationClip, (float)(keyShift * clip.timeScale));
                    }
                }
                else
                {
                    clip.clipIn += timeOffset * clip.timeScale;
                }
            }

            clip.start = time;
            clip.duration = duration;
        }

        public static void SetEnd(TimelineClip clip, double time, bool affectTimeScale)
        {
            var duration = Math.Max(time - clip.start, TimelineClip.kMinDuration);

            if (affectTimeScale && clip.SupportsSpeedMultiplier())
            {
                var f = clip.duration / duration;
                clip.timeScale *= f;
            }

            clip.duration = duration;
        }

        public static bool ResetEditing(TimelineClip[] clips)
        {
            var result = false;

            foreach (var clip in clips)
                result = result || ResetEditing(clip);

            return result;
        }

        public static bool ResetEditing(TimelineClip clip)
        {
            if (clip.asset == null)
                return false;

            TimelineUndo.PushUndo(clip.parentTrack, "Reset Clip Editing");

            clip.clipIn = 0.0;

            if (clip.clipAssetDuration < double.MaxValue)
            {
                var duration = clip.clipAssetDuration / clip.timeScale;
                TrimClipWithEditMode(clip, TrimEdge.End, clip.start + duration);
            }

            return true;
        }

        public static bool MatchContent(TimelineClip[] clips)
        {
            var result = false;

            foreach (var clip in clips)
                result = result || MatchContent(clip);

            return result;
        }

        public static bool MatchContent(TimelineClip clip)
        {
            if (clip.asset == null)
                return false;

            TimelineUndo.PushUndo(clip.parentTrack, "Match Clip Content");

            var newStartCandidate = clip.start - clip.clipIn / clip.timeScale;
            var newStart = newStartCandidate < 0.0 ? 0.0 : newStartCandidate;

            TrimClipWithEditMode(clip, TrimEdge.Start, newStart);

            // In case resetting the start was blocked by edit mode or timeline start, we do the best we can
            clip.clipIn = (clip.start - newStartCandidate) * clip.timeScale;
            if (clip.clipAssetDuration > 0 && TimelineHelpers.HasUsableAssetDuration(clip))
            {
                var duration = TimelineHelpers.GetLoopDuration(clip);
                var offset = (clip.clipIn / clip.timeScale) % duration;
                TrimClipWithEditMode(clip, TrimEdge.End, clip.start - offset + duration);
            }

            return true;
        }

        public static void TrimClipWithEditMode(TimelineClip clip, TrimEdge edge, double time)
        {
            var clipItem = ItemsUtils.ToItem(clip);
            EditMode.BeginTrim(clipItem, edge);
            if (edge == TrimEdge.Start)
                EditMode.TrimStart(clipItem, time);
            else
                EditMode.TrimEnd(clipItem, time, false);
            EditMode.FinishTrim();
        }

        public static bool CompleteLastLoop(TimelineClip[] clips)
        {
            foreach (var clip in clips)
            {
                CompleteLastLoop(clip);
            }

            return true;
        }

        public static void CompleteLastLoop(TimelineClip clip)
        {
            FixLoops(clip, true);
        }

        public static bool TrimLastLoop(TimelineClip[] clips)
        {
            foreach (var clip in clips)
            {
                TrimLastLoop(clip);
            }

            return true;
        }

        public static void TrimLastLoop(TimelineClip clip)
        {
            FixLoops(clip, false);
        }

        static void FixLoops(TimelineClip clip, bool completeLastLoop)
        {
            if (!TimelineHelpers.HasUsableAssetDuration(clip))
                return;

            var loopDuration = TimelineHelpers.GetLoopDuration(clip);
            var firstLoopDuration = loopDuration - clip.clipIn * (1.0 / clip.timeScale);

            // Making sure we don't trim to zero
            if (!completeLastLoop && firstLoopDuration > clip.duration)
                return;

            var numLoops = (clip.duration - firstLoopDuration) / loopDuration;
            var numCompletedLoops = Math.Floor(numLoops);

            if (!(numCompletedLoops < numLoops))
                return;

            if (completeLastLoop)
                numCompletedLoops += 1;

            var newEnd = clip.start + firstLoopDuration + loopDuration * numCompletedLoops;

            TimelineUndo.PushUndo(clip.parentTrack, "Trim Clip Last Loop");

            TrimClipWithEditMode(clip, TrimEdge.End, newEnd);
        }

        public static bool DoubleSpeed(TimelineClip[] clips)
        {
            foreach (var clip in clips)
            {
                if (clip.SupportsSpeedMultiplier())
                {
                    TimelineUndo.PushUndo(clip.parentTrack, "Double Clip Speed");
                    clip.timeScale = clip.timeScale * 2.0f;
                }
            }

            return true;
        }

        public static bool HalfSpeed(TimelineClip[] clips)
        {
            foreach (var clip in clips)
            {
                if (clip.SupportsSpeedMultiplier())
                {
                    TimelineUndo.PushUndo(clip.parentTrack, "Half Clip Speed");
                    clip.timeScale = clip.timeScale * 0.5f;
                }
            }

            return true;
        }

        public static bool ResetSpeed(TimelineClip[] clips)
        {
            foreach (var clip in clips)
            {
                if (clip.timeScale != 1.0)
                {
                    TimelineUndo.PushUndo(clip.parentTrack, "Reset Clip Speed");
                    clip.timeScale = 1.0;
                }
            }

            return true;
        }
    }
}
